<?php
/**
 * DMFramework
 * 
 * @license Copyright 2012 Dillen Meijboom under the Apache license version 2.0 (http://apache.org/licenses/LICENSE-2.0)
 * @author Dillen Meijboom <info@dillenm.nl>
 * @url http://dmframework.nl
 */

/**
 * The form handler can be used to generate, validate and filter HTML forms.
 * 
 * @license Copyright 2012 Dillen Meijboom under the Apache license version 2.0 (http://apache.org/licenses/LICENSE-2.0)
 * @author Dillen Meijboom <info@dillenm.nl>
 * @package System.Library
 */
class Dmf_Form extends Dmf_Form_Element_Form
{
    /**
     * The name of this form
     * 
     * @var string 
     */
    private $_form_name;
   
    /**
     * All form elements
     * 
     * @var array
     */
    protected $_elements = array();
    
    /**
     * Is the form already validated?
     * 
     * @var boolean
     */
    private $_validated = false;
    
    /**
     * Well, is the form okay or does it contain errors?
     * 
     * @var boolean
     */
    private $_validateResult;
    
    /**
     * Order of the element
     * 
     * @var int 
     */
    public $order = -1;
    
    protected $_nameMultiple = array();
    
    /**
     * Constructor
     * 
     * @param mixed $name 
     */
    public function __construct( $name = false )
    {
        $this->_form_name = $name;
        $this->form = $this;
        
        $this->addElement( 'text' )
            -> setAttr( 'type', 'hidden' )
            -> setAttr( 'name', 'form_' . $name )
            -> setValue( 'posted' );
    }
    
    /**
     * Get a POST value
     * 
     * @param string $index 
     * @return string
     */
    public function __get( $index )
    {
        return $this->data( $index );
    }
    
    /**
     * Combine multiple elements to a decorator
     * 
     * @param array|string $decorators
     * @param array $elements 
     * @return Dmf_Form_Element
     */
    public function combine( $decorators, $elements )
    {
        $element = $this -> addElement( 'empty' )
                         -> addElements( $elements )
                         -> addDecorators( $decorators );
        
        $this->addElement( $element );
        
        foreach( $elements as $element )
        {
            $element = $this->element( $element );
            
            $element->delete();
        }
        
        return $element;
    }
    
    /**
     * Reset the full form 
     */
    public function reset()
    {
        $this->_elements = array();
    }
    
    /**
     * Load a model in to a form
     * 
     * @param object $model
     * @param array $exceptions 
     */
    public function load( $model, $exceptions = array() )
    {
        foreach( $this->_elements as $element )
        {
            $index = $element->getAttr('name');
            
            if ( $element instanceof Dmf_Form_Element_Empty )
            {
                foreach( $element->getElements() as $innerElement )
                {
                    if ( in_array( $innerElement->getAttr( 'name' ), $exceptions ) )
                    {
                        continue;
                    }
                }
            }
            
            if ( $index === null || in_array( $index, $exceptions ) || $index == 'form_' . $this->_form_name ) continue;
            
            $element->setValue( $model->$index );
        }
    }
    
    /**
     * Get all elements
     * 
     * @return array 
     */
    public function getElements()
    {
        $elements = array();
        
        foreach( $this->_elements as $element )
        {
            if ( $element instanceof Dmf_Form_Element_Form )
            {
                continue;
            }
            
            $elements[ $element->getOrder() ] = $element;
        }
        
        ksort( $elements );
        
        return $elements;
    }
    
    /**
     * Set form action (attribute)
     * 
     * @param string $action 
     */
    public function setAction( $action )
    {
        $this->setAttr( 'action', $action );
    }
    
    /**
     * Create and return element
     * 
     * @param Dmf_Form_Element|string $type
     * @return Dmf_Form_Element 
     */
    public function element( $type )
    {
        $class = 'Dmf_Form_Element_' . ucfirst( $type );
        
        if ( $type instanceof Dmf_Form_Element )
        {
            $element = $type;
        }
        elseif( $this->getByName( $type ) !== null )
        {
            $element = $this->getByName( $type );
        }
        else
        {
            $element = new $class();
        }
        
        $element->form = $this;

        return $element;
    }
    
    /**
     * Add widget to the form
     * 
     * @param string $name
     * @return Dmf_Form_Element_Widget
     * @throws Exception 
     */
    public function addWidget( $name )
    {
        try
        {
            $element = new Dmf_Form_Element_Widget( $name );
            
            $this->_elements[] = $element;
            
            $element->form = $this;
            $element->order( $this->order );
            
            $this->order++;
        }
        catch( Exception $e )
        {
            throw new Exception( 'Element: "' . $type . '" does not exist in the form handler.' );
        }
        
        return $element;
    }
    
    /**
     * Add an element
     * 
     * @param mixed $type
     * @return Dmf_Form_Element
     * @throws Exception 
     */
    public function addElement( $type )
    {
        try
        {
            $element = $this->element( $type );
            
            $this->_elements[] = $element;
            
            $element->order( $this->order );
            
            $this->order++;
        }
        catch( Exception $e )
        {
            throw new Exception( 'Element: "' . $type . '" does not exist in the form handler.' );
        }
        
        return $element;
    }
    
    /**
     * Shortcut to addElement( 'button' )
     * 
     * @param string $value
     * @return Dmf_Form_Element
     */
    public function addButton( $value )
    {
        return $this->addElement( 'button' )->setAttr( 'value', $value )->setAttr( 'type', 'submit' );
    }
    
    /**
     * Get an element by name
     * 
     * @param string $name
     * @return Dmf_Form_Element 
     */
    public function getByName( $name )
    {
        foreach( $this->_elements as $element )
        {
            if ( $element instanceof Dmf_Form_Element_Empty )
            {
                foreach( $element->getElements() as $innerElement )
                {
                    if ( $innerElement->getAttr( 'name' ) == $name )
                    {
                        return $innerElement;
                    }
                }
            }
            else
            {
                if ( $element->getAttr( 'name' ) == $name )
                {
                    return $element;
                }
            }
        }
        
        return null;
    }
    
    /**
     * Modify the $_FILES array and return it.
     * 
     * @return array
     */
    public function getModifiedFilesArray()
    {
        $files_array = array();

        foreach( $_FILES as $key => $val )
        {
            if ( is_array( $val['name'] ) )
            {
                for ( $i = 0; $i < count( $val['name'] ); $i++ )
                {
                    $files_array[ $key . '[' . $i . ']' ]['name'] = $val['name'][ $i ];
                }

                for ( $i = 0; $i < count( $val['type'] ); $i++ )
                {
                    $files_array[ $key . '[' . $i . ']' ]['type'] = $val['type'][ $i ];
                }

                for ( $i = 0; $i < count( $val['tmp_name'] ); $i++ )
                {
                    $files_array[ $key . '[' . $i . ']' ]['tmp_name'] = $val['tmp_name'][ $i ];
                }

                for ( $i = 0; $i < count( $val['size'] ); $i++ )
                {
                    $files_array[ $key . '[' . $i . ']' ]['size'] = $val['size'][ $i ];
                }

                for ( $i = 0; $i < count( $val['error'] ); $i++ )
                {
                    $files_array[ $key . '[' . $i . ']' ]['error'] = $val['error'][ $i ];
                }
            }
            else
            {
                $files_array[ $key ] = $val;
            }
        }

        return $files_array;
    }
    
    /**
     * Validat the form but don't execute anything 
     */
    public function validate( $array = false )
    {
        //Weird bug fix.
        if ( is_bool( $this->_validateResult ) )
        {
            return;
        }
        
        $this->_validated = true;
        $this->_validateResult = true;
        
        foreach( ( $array === false ? array_merge( $_POST, $this->getModifiedFilesArray() ) : $array ) as $index => $value )
        {
            $element = $this->getByName( $index );
            
            if ( is_array( $value ) && ! isset( $files_array[ $index ] ) )
            {
                $array = array();
                
                foreach( $value as $key => $val )
                {
                    $array[ $index . '[' . $key . ']' ] = $val;
                }
                
                $this->validate( $array );
            }
            
            if ( $element === null ) continue;
            
            foreach( $element->getFilters() as $filter )
            {
                $value = $filter->getValue( $value );
            }
            
            $element->setValue( $value );
            
            if ( $element->getType() == 'checkbox' )
            {
                $element->setAttr( 'checked' );
            }
            
            if ( $element->getType() == 'checkbox' && $element->getAttr('value') == null )
            {
                $element->unsetAttr( 'checked' );
            }
            
            foreach( $element->getHelpers() as $helper )
            {
                if ( $helper->execute( $value ) === false )
                {
                    //Not implemented yet
                }
            }
            
            foreach( $element->getValidators() as $validator )
            {
                if ( ! $validator->isValid( $value ) )
                {
                    $this->_validateResult = false;
                    
                    $element->addError( $validator->getType(), $validator->getMessage() );
                }
            }
        }
    }
    
    /**
     * Get form data in a result object
     * 
     * @param mixed $index
     * @return boolean|Dmf_Form_Result 
     */
    public function data( $index = null )
    {
        if ( ! $this->isPosted() )
        {
            return false;
        }
        
        $data = array();
        $files = $this->getModifiedFilesArray();
        
        foreach( $this->_elements as $element )
        {
            $name = $element->getAttr( 'name' );
            
            if ( substr( $name, 0, 5 ) == 'form_' || ! ( isset( $_POST[ $element->getAttr( 'name' ) ] ) || isset( $_FILES[ $element->getAttr( 'name' ) ] ) ) )
            {
                continue;
            }
            
            $std = new Dmf_Stdclass;
            $postValue = isset( $_POST[ $element->getAttr( 'name' ) ] ) ? $_POST[ $element->getAttr( 'name' ) ] : $files[ $element->getAttr( 'name' ) ];
            
            switch( $element->getType() )
            {
                case 'checkbox':
                    $value = ( $postValue == 'On' ? true : false );
                    
                    $std->isChecked = function( $obj )
                    {
                        return ( $obj->value == true ? true : false );
                    };
                    
                    break;
                default:
                    $value = $postValue;
                    break;
            }
            
            $std->value = $value;
            $std->getName = function( $obj )
            {
                $array = explode( '_', get_class( $obj ) );
                
                return end( $array );
            };
            $std->getValue = function( $obj )
            {
                return $obj->value;
            };
            
            if ( $index != null && $index !== true && $index == $element->getAttr( 'name' ) )
            {
                return $std;
            }
            
            $data[ $name ] = $std;
        }
        
        if ( $index === true )
        {
            return $data;
        }
        else
        {
            return Dmf_Stdclass::fromArray( $data );
        }
    }
    
    /**
     * Is this form posted?
     * 
     * @return boolean 
     */
    public function isPosted()
    {
        if ( strtolower( $_SERVER['REQUEST_METHOD'] ) == 'post' && isset( $_POST['form_' . $this->_form_name ] ) && $_POST['form_' . $this->_form_name ] == 'posted' )
        {
            return true;
        }
        
        return false;
    }
    
    /**
     * Is the form valid?
     * 
     * @return boolean 
     */
    public function isValid()
    {
        if ( ! $this->isPosted() )
        {
            return false;
        }
        
        if ( ! $this->_validated && ! is_bool( $this->_validateResult ) )
        {
            $this->validate();
        }
        
        return $this->_validateResult;
    }
    
    /**
     * Render the form
     * 
     * @return string 
     */
    public function render()
    {
        $result = '';
        $elements = $this->getElements();
        
        foreach( $elements as $element )
        {
            $result .= $element->render();
        }
        
        $this->setValue( $result );
        
        $html = new Dmf_Html();
        
        return $html->cleanup( parent::render() );
    }
    
    /**
     * Set data to model
     * 
     * @param object $model
     * @param array $exceptions
     * @return object 
     */
    public function setData( $model, $exceptions = array() )
    {
        foreach( $this->_elements as $element )
        {
            if ( in_array( $element->getAttr( 'name' ), $exceptions ) || $element->getAttr( 'name' ) == null || substr( $element->getAttr( 'name' ), 0, 5 ) == 'form_' )
            {
                continue;
            }
            
            if ( $element instanceof Dmf_Form_Element_Empty )
            {
                foreach( $element->getElements() as $innerElement )
                {
                    if ( in_array( $innerElement->getAttr( 'name' ), $exceptions ) )
                    {
                        continue;
                    }
                }
            }
            
            $index = $element->getAttr( 'name' );
            $model->$index = $element->getAttr( 'value' );
        }
        
        return $model;
    }
    
    /**
     * Create the form into a db-table
     * 
     * @param object $model
     * @param array $exceptions 
     */
    public function create( $model, $exceptions = array() )
    {
        $this->setData( $model, $exceptions )->create();
    }
    
    /**
     * Save the form to a db-table
     * 
     * @param object $model
     * @param array $exceptions 
     */
    public function save( $model, $exceptions = array() )
    {
        $this->setData( $model, $exceptions )->save();
    }
    
    /**
     * Delete an element
     * 
     * @param Dmf_Form_Element $element 
     */
    public function deleteElement( $element )
    {
        $element = $this->element( $element );
        
        foreach( $this->_elements as &$el )
        {
            if ( $el === $element )
            {
                $el = null;
                
                $this->_elements = array_filter( $this->_elements );
                break;
            }
        }
    }
    
    /**
     * Shortcut $this->render()
     * 
     * @return string
     */
    public function __toString()
    {
        return $this->render();
    }
}